package sk.upjs.ariadne.editor.maplayers;

import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.event.MouseEvent;
import java.awt.geom.Ellipse2D;
import java.awt.geom.Line2D;
import java.awt.geom.Point2D;
import java.awt.geom.Rectangle2D;
import java.util.*;

import javax.naming.event.EventDirContext;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;

import sk.upjs.ariadne.editor.MapLayer;
import sk.upjs.ariadne.editor.data.*;

public class WallLayer extends MapLayer {

	// ----------------------------------------------------------------------
	// Constants
	// ----------------------------------------------------------------------

	private static final int CLOSE_DISTANCE = 5;

	private static final int NODE_RADIUS = 2;

	// ----------------------------------------------------------------------
	// GUI - instance variables
	// ----------------------------------------------------------------------

	/**
	 * A panel containing controls that manage editing of this layer.
	 */
	private WallLayerControl controlPanel;

	/**
	 * Edit modes for the layer
	 */
	public enum EditMode {
		/**
		 * User can select map elements
		 */
		SELECTION,
		/**
		 * User adds new nodes connected by walls
		 */
		WALL_INSERTION
	}

	/**
	 * Current edit mode
	 */
	private EditMode editMode = EditMode.WALL_INSERTION;

	// ----------------------------------------------------------------------
	// Map elements - instance variables
	// ----------------------------------------------------------------------

	/**
	 * Edited indoor map
	 */
	IndoorMap map;

	// ----------------------------------------------------------------------
	// Editing - instance variables
	// ----------------------------------------------------------------------

	/**
	 * Selected layer element
	 */
	private MapElement selectedElement = null;

	/**
	 * Indicates whether there is an open wall drawing in the wall insertion
	 * mode
	 */
	private boolean wallDrawing = false;

	/**
	 * Last known mouse coordinates
	 */
	private Point2D lastMouseCoordinates = null;

	// ----------------------------------------------------------------------
	// Constructors
	// ----------------------------------------------------------------------

	/**
	 * Constructs a new wall map layer
	 */
	public WallLayer(IndoorMap map) {
		if (map == null)
			throw new NullPointerException("It is not possible to construct a WallLayer without indoor map to be edited.");
		
		this.map = map;
		controlPanel = new WallLayerControl(this);
	}

	// ----------------------------------------------------------------------
	// Painting
	// ----------------------------------------------------------------------

	@Override
	public void paint(Graphics2D g, Rectangle2D area) {
		// draw walls
		for (MapWall wall : map.getWalls())
			drawWall(g, area, wall);

		// draw nodes
		for (MapNode node : map.getNodes())
			drawNode(g, area, node);
	}

	@Override
	public void paintEditOverlay(Graphics2D g, Rectangle2D area) {
		if (editMode.equals(EditMode.WALL_INSERTION)) {
			paintWallInsertionOverlay(g, area);
			return;
		}

		if (editMode.equals(EditMode.SELECTION)) {
			paintSelectionOverlay(g, area);
			return;
		}
	}

	/**
	 * Paints a wall
	 */
	private void drawWall(Graphics2D g, Rectangle2D area, MapWall wall) {
		double zoom = getParentZoom();

		g.setPaint(Color.yellow);
		g.setStroke(new BasicStroke(1));

		MapNode startPoint = wall.getStartPoint();
		MapNode endPoint = wall.getEndPoint();
		Line2D line = new Line2D.Double(startPoint.getX() * zoom,
				startPoint.getY() * zoom, endPoint.getX() * zoom,
				endPoint.getY() * zoom);
		g.draw(line);
	}

	/**
	 * Paints a cross in the middle of the wall
	 */
	private void drawMiddleCross(Graphics2D g, Rectangle2D area, MapWall wall) {
		double zoom = getParentZoom();
		g.setPaint(Color.blue);
		g.setStroke(new BasicStroke(1));

		MapNode startPoint = wall.getStartPoint();
		MapNode endPoint = wall.getEndPoint();

		Point2D center = new Point2D.Double(zoom
				* (startPoint.getX() + endPoint.getX()) / 2, zoom
				* (startPoint.getY() + endPoint.getY()) / 2);

		g.draw(new Line2D.Double(center.getX() - NODE_RADIUS, center.getY()
				- NODE_RADIUS, center.getX() + NODE_RADIUS, center.getY()
				+ NODE_RADIUS));
		g.draw(new Line2D.Double(center.getX() + NODE_RADIUS, center.getY()
				- NODE_RADIUS, center.getX() - NODE_RADIUS, center.getY()
				+ NODE_RADIUS));
	}

	/**
	 * Paints a node
	 */
	private void drawNode(Graphics2D g, Rectangle2D area, MapNode node) {
		double zoom = getParentZoom();

		g.setPaint(Color.red);
		g.setStroke(new BasicStroke(1));

		double cx = node.getX() * zoom;
		double cy = node.getY() * zoom;
		g.draw(new Ellipse2D.Double(cx - NODE_RADIUS, cy - NODE_RADIUS,
				2 * NODE_RADIUS, 2 * NODE_RADIUS));
	}

	/**
	 * Paints edit overlay in case of active wall insertion
	 */
	private void paintWallInsertionOverlay(Graphics2D g, Rectangle2D area) {
		if ((!wallDrawing) || (selectedElement == null))
			return;

		double zoom = getParentZoom();

		g.setPaint(Color.yellow);
		g.setStroke(new BasicStroke(1));

		MapNode selectedNode = (MapNode) selectedElement;
		g.draw(new Line2D.Double(selectedNode.getX() * zoom, selectedNode
				.getY() * zoom, lastMouseCoordinates.getX() * zoom,
				lastMouseCoordinates.getY() * zoom));
	}

	/**
	 * Paints edit overlay in case of active selection mode
	 */
	private void paintSelectionOverlay(Graphics2D g, Rectangle2D area) {
		// draw cross points over all walls
		for (MapWall wall : map.getWalls())
			drawMiddleCross(g, area, wall);
	}

	// ----------------------------------------------------------------------
	// Editing
	// ----------------------------------------------------------------------

	@Override
	protected void onMouseReleased(Point2D coordinates, MouseEvent e) {
		// check click in the path insertion mode
		if (SwingUtilities.isLeftMouseButton(e)
				&& editMode.equals(EditMode.WALL_INSERTION)) {
			wallInsertionClick(coordinates, e);
			return;
		}

		lastMouseCoordinates = coordinates;
	}

	@Override
	protected void onMouseClicked(Point2D coordinates, MouseEvent e) {
		// check double-click in the path insertion mode
		if (SwingUtilities.isLeftMouseButton(e)
				&& editMode.equals(EditMode.WALL_INSERTION)
				&& (e.getClickCount() >= 2)) {
			wallDrawing = false;
			repaint();
			return;
		}
	}

	@Override
	protected void onMouseMoved(Point2D coordinates, MouseEvent e) {
		if (editMode.equals(EditMode.WALL_INSERTION) && wallDrawing) {
			repaint();
		}

		lastMouseCoordinates = coordinates;
	}

	@Override
	protected void onMouseDragged(Point2D coordinates, MouseEvent e) {
		lastMouseCoordinates = coordinates;
	}

	/**
	 * Handles a wall insertion click
	 */
	private void wallInsertionClick(Point2D coordinates, MouseEvent e) {
		// if this is the first click in the wall drawing, initiate it
		if (wallDrawing == false) {
			wallDrawing = true;
			selectedElement = null;
		}

		// use close node or create a new node
		MapNode clickNode = findCloseNode(coordinates);
		if (clickNode == null) {
			clickNode = new MapNode(coordinates.getX(), coordinates.getY());
			map.getNodes().add(clickNode);
		}

		// add a new wall
		if ((selectedElement != null) && (selectedElement != clickNode)) {
			MapWall wall = new MapWall((MapNode) selectedElement, clickNode);
			map.getWalls().add(wall);
		}

		selectedElement = clickNode;

		repaint();
	}

	/**
	 * Returns the closest node among nodes with distance smaller than
	 * CLOSE_DISTANCE
	 */
	private MapNode findCloseNode(Point2D coordinates) {
		// find the closest node
		MapNode result = null;
		double bestDistance = Double.MAX_VALUE;
		double nodeDistance;
		for (MapNode node : map.getNodes()) {
			nodeDistance = coordinates.distance(node.getX(), node.getY());
			if ((result == null) || (nodeDistance < bestDistance)) {
				result = node;
				bestDistance = nodeDistance;
			}
		}

		if (result == null)
			return null;

		// check whether distance of the closest node is "visually close"
		if (bestDistance * getParentZoom() <= CLOSE_DISTANCE)
			return result;
		else
			return null;
	}

	/**
	 * Finishes an open edit action
	 */
	private void finishEditActions() {
		if (editMode.equals(EditMode.WALL_INSERTION) && wallDrawing) {
			wallDrawing = false;
		}
	}

	// ----------------------------------------------------------------------
	// Controlling
	// ----------------------------------------------------------------------

	/**
	 * Sets edit mode of this layer.
	 * 
	 * @param mode
	 *            the desired edit mode.
	 */
	public void setEditMode(EditMode mode) {
		if (editMode.equals(mode))
			return;

		if (mode == null)
			throw new NullPointerException("Edit mode cannot be null.");

		finishEditActions();
		editMode = mode;
		repaint();
	}

	/**
	 * Returns current edit mode of this layer.
	 * 
	 * @return edit mode of this layer.
	 */
	public EditMode getEditMode() {
		return editMode;
	}

	@Override
	public JPanel getControlPanel() {
		return controlPanel;
	}

	@Override
	public String getLayerName() {
		return "Wall layer";
	}

	@Override
	protected void onEditFinish() {
		finishEditActions();
	}
}
